(function (global, factory) {
  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
  typeof define === 'function' && define.amd ? define(factory) :
  (global.OnScreen = factory());
}(this, function () { 'use strict';

  /**
   * Attaches the scroll event handler
   *
   * @return {void}
   */
  function attach() {
      var container = this.options.container;

      if (container instanceof HTMLElement) {
          var style = window.getComputedStyle(container);

          if (style.position === 'static') {
              container.style.position = 'relative';
          }
      }

      container.addEventListener('scroll', this._scroll, { passive: true });
      window.addEventListener('resize', this._scroll, { passive: true });
      this._scroll();
      this.attached = true;
  }

  /**
   * Checks an element's position in respect to the viewport
   * and determines wether it's inside the viewport.
   *
   * @param {node} element The DOM node you want to check
   * @return {boolean} A boolean value that indicates wether is on or off the viewport.
   */
  function inViewport(el) {
      var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : { tolerance: 0 };

      if (!el) {
          throw new Error('You should specify the element you want to test');
      }

      if (typeof el === 'string') {
          el = document.querySelector(el);
      }

      var elRect = el.getBoundingClientRect();

      return (
          // Check bottom boundary
          elRect.bottom - options.tolerance > 0 &&

          // Check right boundary
          elRect.right - options.tolerance > 0 &&

          // Check left boundary
          elRect.left + options.tolerance < (window.innerWidth || document.documentElement.clientWidth) &&

          // Check top boundary
          elRect.top + options.tolerance < (window.innerHeight || document.documentElement.clientHeight)
      );
  }

  /**
   * Checks an element's position in respect to a HTMLElement
   * and determines wether it's within its boundaries.
   *
   * @param {node} element The DOM node you want to check
   * @return {boolean} A boolean value that indicates wether is on or off the container.
   */
  function inContainer(el) {
      var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : { tolerance: 0, container: '' };

      if (!el) {
          throw new Error('You should specify the element you want to test');
      }

      if (typeof el === 'string') {
          el = document.querySelector(el);
      }
      if (typeof options === 'string') {
          options = {
              tolerance: 0,
              container: document.querySelector(options)
          };
      }
      if (typeof options.container === 'string') {
          options.container = document.querySelector(options.container);
      }
      if (options instanceof HTMLElement) {
          options = {
              tolerance: 0,
              container: options
          };
      }
      if (!options.container) {
          throw new Error('You should specify a container element');
      }

      var containerRect = options.container.getBoundingClientRect();

      return (
          // // Check bottom boundary
          el.offsetTop + el.clientHeight - options.tolerance > options.container.scrollTop &&

          // Check right boundary
          el.offsetLeft + el.clientWidth - options.tolerance > options.container.scrollLeft &&

          // Check left boundary
          el.offsetLeft + options.tolerance < containerRect.width + options.container.scrollLeft &&

          // // Check top boundary
          el.offsetTop + options.tolerance < containerRect.height + options.container.scrollTop
      );
  }

  // TODO: Refactor this so it can be easily tested
  /* istanbul ignore next */
  function eventHandler() {
      var trackedElements = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
      var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : { tolerance: 0 };

      var selectors = Object.keys(trackedElements);
      var testVisibility = void 0;

      if (!selectors.length) return;

      if (options.container === window) {
          testVisibility = inViewport;
      } else {
          testVisibility = inContainer;
      }

      selectors.forEach(function (selector) {
          trackedElements[selector].nodes.forEach(function (item) {
              if (testVisibility(item.node, options)) {
                  item.wasVisible = item.isVisible;
                  item.isVisible = true;
              } else {
                  item.wasVisible = item.isVisible;
                  item.isVisible = false;
              }
              if (item.isVisible === true && item.wasVisible === false) {
                  if (!trackedElements[selector].enter) return;

                  Object.keys(trackedElements[selector].enter).forEach(function (callback) {
                      if (typeof trackedElements[selector].enter[callback] === 'function') {
                          trackedElements[selector].enter[callback](item.node, 'enter');
                      }
                  });
              }
              if (item.isVisible === false && item.wasVisible === true) {
                  if (!trackedElements[selector].leave) return;

                  Object.keys(trackedElements[selector].leave).forEach(function (callback) {
                      if (typeof trackedElements[selector].leave[callback] === 'function') {
                          trackedElements[selector].leave[callback](item.node, 'leave');
                      }
                  });
              }
          });
      });
  }

  /**
   * Debounces the scroll event to avoid performance issues
   *
   * @return {void}
   */
  function debouncedScroll() {
      var _this = this;

      var timeout = void 0;

      return function () {
          clearTimeout(timeout);

          timeout = setTimeout(function () {
              eventHandler(_this.trackedElements, _this.options);
          }, _this.options.debounce);
      };
  }

  /**
   * Removes the scroll event handler
   *
   * @return {void}
   */
  function destroy() {
    this.options.container.removeEventListener('scroll', this._scroll);
    window.removeEventListener('resize', this._scroll);
    this.attached = false;
  }

  /**
   * Stops tracking elements matching a CSS selector. If a selector has no
   * callbacks it gets removed.
   *
   * @param {string} event The event you want to stop tracking (enter or leave)
   * @param {string} selector The CSS selector you want to stop tracking
   * @return {void}
   */
  function off(event, selector, handler) {
      var enterCallbacks = Object.keys(this.trackedElements[selector].enter || {});
      var leaveCallbacks = Object.keys(this.trackedElements[selector].leave || {});

      if ({}.hasOwnProperty.call(this.trackedElements, selector)) {
          if (handler) {
              if (this.trackedElements[selector][event]) {
                  var callbackName = typeof handler === 'function' ? handler.name : handler;
                  delete this.trackedElements[selector][event][callbackName];
              }
          } else {
              delete this.trackedElements[selector][event];
          }
      }

      if (!enterCallbacks.length && !leaveCallbacks.length) {
          delete this.trackedElements[selector];
      }
  }

  /**
   * Starts tracking elements matching a CSS selector
   *
   * @param {string} event The event you want to track (enter or leave)
   * @param {string} selector The element you want to track
   * @param {function} callback The callback function to handle the event
   * @return {void}
   */
  function on(event, selector, callback) {
      var allowed = ['enter', 'leave'];

      if (!event) throw new Error('No event given. Choose either enter or leave');
      if (!selector) throw new Error('No selector to track');
      if (allowed.indexOf(event) < 0) throw new Error(event + ' event is not supported');

      if (!{}.hasOwnProperty.call(this.trackedElements, selector)) {
          this.trackedElements[selector] = {};
      }

      this.trackedElements[selector].nodes = [];

      for (var i = 0, elems = document.querySelectorAll(selector); i < elems.length; i++) {
          var item = {
              isVisible: false,
              wasVisible: false,
              node: elems[i]
          };

          this.trackedElements[selector].nodes.push(item);
      }

      if (typeof callback === 'function') {
          if (!this.trackedElements[selector][event]) {
              this.trackedElements[selector][event] = {};
          }

          this.trackedElements[selector][event][callback.name || 'anonymous'] = callback;
      }
  }

  /**
   * Observes DOM mutations and runs a callback function when
   * detecting one.
   *
   * @param {node} obj The DOM node you want to observe
   * @param {function} callback The callback function you want to call
   * @return {void}
   */
  function observeDOM(obj, callback) {
      var MutationObserver = window.MutationObserver || window.WebKitMutationObserver;

      /* istanbul ignore else */
      if (MutationObserver) {
          var obs = new MutationObserver(callback);

          obs.observe(obj, {
              childList: true,
              subtree: true
          });
      } else {
          obj.addEventListener('DOMNodeInserted', callback, false);
          obj.addEventListener('DOMNodeRemoved', callback, false);
      }
  }

  /**
   * Detects wether DOM nodes enter or leave the viewport
   *
   * @constructor
   * @param {object} options The configuration object
   */
  function OnScreen() {
      var _this = this;

      var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : { tolerance: 0, debounce: 100, container: window };

      this.options = {};
      this.trackedElements = {};

      Object.defineProperties(this.options, {
          container: {
              configurable: false,
              enumerable: false,
              get: function get() {
                  var container = void 0;

                  if (typeof options.container === 'string') {
                      container = document.querySelector(options.container);
                  } else if (options.container instanceof HTMLElement) {
                      container = options.container;
                  }

                  return container || window;
              },
              set: function set(value) {
                  options.container = value;
              }
          },
          debounce: {
              get: function get() {
                  return parseInt(options.debounce, 10) || 100;
              },
              set: function set(value) {
                  options.debounce = value;
              }
          },
          tolerance: {
              get: function get() {
                  return parseInt(options.tolerance, 10) || 0;
              },
              set: function set(value) {
                  options.tolerance = value;
              }
          }
      });

      Object.defineProperty(this, '_scroll', {
          enumerable: false,
          configurable: false,
          writable: false,
          value: this._debouncedScroll.call(this)
      });

      observeDOM(document.querySelector('body'), function () {
          Object.keys(_this.trackedElements).forEach(function (element) {
              _this.on('enter', element);
              _this.on('leave', element);
          });
      });

      this.attach();
  }

  Object.defineProperties(OnScreen.prototype, {
      _debouncedScroll: {
          configurable: false,
          writable: false,
          enumerable: false,
          value: debouncedScroll
      },
      attach: {
          configurable: false,
          writable: false,
          enumerable: false,
          value: attach
      },
      destroy: {
          configurable: false,
          writable: false,
          enumerable: false,
          value: destroy
      },
      off: {
          configurable: false,
          writable: false,
          enumerable: false,
          value: off
      },
      on: {
          configurable: false,
          writable: false,
          enumerable: false,
          value: on
      }
  });

  OnScreen.check = inViewport;

  return OnScreen;

}));
//# sourceMappingURL=on-screen.umd.js.map
